/**
 * @file    drv.c
 * @Author  Riccardo Manfrin [namesurname at gmail dot com]
 * @brief   Driver functionalities implemented for the char device (circular buffer).
 */


#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>      //define the operations on a file [struct file_operations]
#include <linux/cdev.h>    //register fops to char device [cdev_alloc, cdev_add]
#include <linux/string.h>  //memset
#include <linux/version.h> //LINUX_VERSION_CODE
#include <asm/uaccess.h>   //communicate with userspace [copy_to_user, copy_from_user]
#include "drv.h"
#include "defines.h"

ssize_t dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
ssize_t dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
int dev_open(struct inode *inode, struct file *filp);
int dev_release(struct inode *inode, struct file *filp);
int dev_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

//Operations available for the char dev file.
struct file_operations fops = {
  //C99 syntax
  //specifies the module that owns the structure to ensure that it won't be unloaded while
  //a certain operations is being performed.
  .owner = THIS_MODULE,
  //Struct pointers to module essential functionalities (for file_operations structure).
  .read = dev_read,
  .write = dev_write,
  .open = dev_open,
  .release = dev_release,
  .ioctl = dev_ioctl
};
//Device name
#define DEVNAME "/dev/wifihack" 
//Use dynamic major/minor assignment
static unsigned int minor=0;
static unsigned int major=0;

//Number of char dev to allocate
static int cnt = 1;

//Char device
static struct wh_dev wh_dev;

/**
 * drv_init and drv_close are meant to 
 * allocate and deallocate the char device
 * while dev_open and dev_release are used to 
 * attach / detach such char device.
 */
int wh_drv_reg(unsigned int forced_major, unsigned int buffSz, unsigned int ODoW){
  int result=0;
  //Used to hold a file descriptor associated to the device.
  dev_t wh_dev_num;
  /**
   * This checks if we want to statically define the MAJOR device number or we want kernel
   * to pick up one for us. Behavior can be choosen @ module loading time by setting the 
   * correct value to the relative parameter.
   */ 
  if(forced_major){
    /**
     * MKDEV is a macro defined in linux/kdev_t.h used to generate an identifier (the device
     * number) that univocally points the char device: specifically, every char device is 
     * identified through 32bits, the first 12 of which are used for the MAJOR identifier 
     * (commonly used to specify the type of device). The latter 20bits are used to 
     * univocally identify the specific device of that type.
     */
    wh_dev_num = MKDEV(forced_major, minor);
    major=forced_major;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
    result = register_chrdev_region(wh_dev_num, cnt, DEVNAME);
#else
#error Old kernel version
#endif    
  }
  else{
    /**
     * alloc_chrdev_region is used here to prepare the kernel for hosting 
     * a new [set of] char device, by "reserving" the required major and minor numbers
     * for further device generation.
     * - wh_dev_num will store (major << 20) | minor) [output]
     * - minor defines the starting minor number [input] 
     * - cnt specifies the number of char devs I want to create[input]
     * - DEVNAME is the device name associated with the just reserved device major/minor 
     *   numbers
     * RETURNS x > 0 if procedure went fine.
     */
    result = alloc_chrdev_region(&wh_dev_num, minor, cnt, DEVNAME);
    
    /// Strips the major number from the value just stored in wh_dev_num
    major = MAJOR(wh_dev_num);
  }
  printk(KERN_DEBUG "Device Major = %i, Minor = %i\n", major, minor);
  if(result>=0)
  {
      /**
       * kzalloc (allocate and memset to zero) the space for the cdev and 
       * initialize its relative kobject
       */
      wh_dev.my_cdev = cdev_alloc();
      ///Specifies the owner of the cdev
      wh_dev.my_cdev->owner = THIS_MODULE;
      ///Specify the operations associated to the cdev
      wh_dev.my_cdev->ops = &fops;
      ///Specify the size allocated for the cdev
      wh_dev.size = buffSz;
      printk(KERN_DEBUG "Buffer size = %i\n", buffSz);
      ///Specify the buffer behavior
      wh_dev.ODoW = ODoW;
      if(wh_dev.ODoW) printk(KERN_DEBUG "Buffer overwrite enabled\n");
      
      /**
       * Resetting the read/write offsets for circular buffer
       * we set the "initial" condition to be rd_offset=0 and
       * rw_offset to be 1.
       * We must set a forbidden state for the system and we
       * use this forbidden condition: rd_offset==rw_offset.
       * By doing so we avoid any doubts on the buffer being
       * all to be written or to be read.
       */
      wh_dev.rd_offset=0;
      wh_dev.rw_offset=0;
      /**
       * adds the device represented by wh_dev to the system, making 
       * it live immediately. 
       */
      cdev_add(wh_dev.my_cdev, wh_dev_num, cnt);
      ///Alloc data for the cdev (and clean it)
      wh_dev.data = kmalloc(wh_dev.size,GFP_KERNEL);
      memset(wh_dev.data, 0, wh_dev.size);
  }
  else
  {
    printk(KERN_WARNING "wifihack: can't get major %d\n", major);
  }
  return result;
}

void wh_drv_unreg(void){
  ///Univocally retrieve the dev_t id number from major and minor.
  dev_t wh_dev_num = MKDEV(major, minor);
  ///Unregistering the devices
  unregister_chrdev_region(wh_dev_num, cnt);
  return;
}

ssize_t dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
    int retval=0;
    int readable_bytes = 0;
    int remaining_bytes = 0;
    struct wh_dev *dev = filp->private_data;
    
    /**
     * Buffer sizes check:
     * We need to know how much data has been written to know how muc can be read.
     */
    if(dev->rw_offset > dev->rd_offset)
      readable_bytes = dev->rw_offset - dev->rd_offset;
    else if(dev->rw_offset < dev->rd_offset)
      readable_bytes = dev->size - (dev->rd_offset - dev->rw_offset);
    
    if(readable_bytes<count) return -EFAULT;
    if(count<readable_bytes) readable_bytes = count;
    
    printk("count = %i\treadable_bytes = %i\tremaining_bytes = %i\trw_offset = %i\trd_offset = %i\tsize = %i\tretval = %i\n", count, readable_bytes, remaining_bytes, dev->rw_offset, (int) dev->rd_offset, (int)dev->size, (int)retval);
    
    /**
     * Copy kernel circular buffer data into 
     * the user space buffer.
     */
    if (dev->rw_offset > dev->rd_offset) //One segment to read
    {
      if (copy_to_user(buf, dev->data + dev->rd_offset ,readable_bytes)) 
      {
        retval = -EFAULT;
        goto out;
      }
      retval = readable_bytes;
      dev->rd_offset += readable_bytes;
    }
    else //2 different readable segments (one @the and the latter @the beginning)
    {
      // Is the remaining free buffer (I mean, space before dev->size) enough for what I need?
      if (readable_bytes <= dev->size - dev->rd_offset)
      {
        if (copy_to_user(buf, dev->data + dev->rd_offset , readable_bytes))
        {
          retval = -EFAULT;
          goto out;
        }
        retval = readable_bytes;
        if (readable_bytes < dev->size - dev->rd_offset)
          dev->rd_offset += readable_bytes;
        else
          dev->rd_offset = 0;
      }
      else 
      {
        remaining_bytes = readable_bytes - (dev->size - dev->rd_offset);
        readable_bytes = dev->size - dev->rd_offset;
        if (copy_to_user(buf, dev->data + dev->rd_offset, readable_bytes)) 
        {
          retval = -EFAULT;
          goto out;
        }
        dev->rd_offset = 0;
        retval = readable_bytes;
        if (copy_to_user(buf + readable_bytes, dev->data + dev->rd_offset, remaining_bytes)) 
        {
          retval = -EFAULT;
          goto out;
        }
        retval += remaining_bytes;
        dev->rd_offset += remaining_bytes;
      }
    }
  out:
  {
    return retval;
  }
}

ssize_t dev_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos){
    int retval=0;
    int writable_bytes = 0;
    int remaining_bytes = 0;
    struct wh_dev *dev = filp->private_data;
    
    ///Buffer sizes check
    if(dev->rw_offset < dev->rd_offset) 
      writable_bytes = dev->rd_offset - dev->rw_offset - 1;
    else if(dev->rw_offset >= dev->rd_offset) 
      writable_bytes = dev->size - (dev->rw_offset - dev->rd_offset) - 1;
    
    if(writable_bytes<count)
    {
      if(wh_dev.ODoW)
      {
        if(count<=dev->size)
        {
          writable_bytes = (unsigned int)count;
          dev->rd_offset = ((dev->rw_offset + (unsigned int)count) % dev->size) + 1;
          printk(KERN_DEBUG "Overwriting unread data\n");
        }
        else
        {
          return -EFAULT;
        }
      }
      else
      {
        return -EFAULT;
      }
    }
    else
    {
      writable_bytes = (unsigned int) count;
    }
    
    printk("count = %i\twritable_bytes = %i\tremaining_bytes = %i\trw_offset = %i\trd_offset = %i\tsize = %i\tretval = %i\n", count, writable_bytes, remaining_bytes, dev->rw_offset, (int) dev->rd_offset, (int)dev->size, (int)retval);
    
    /**
     * Here we must copy data coming from the kernel 
     * into the wh_dev.data pointer to then forward
     * it to the userspace.
     */ 
    if (dev->rw_offset < dev->rd_offset)
    {
      if (copy_from_user(dev->data, buff, writable_bytes))
      {
        retval = -EFAULT;
        goto out;
      }
      retval=writable_bytes;
      dev->rw_offset += writable_bytes;
    }
    else //2 different writable segments (one @the and the latter @the beginning)
    {
      // Is the last free part of the buffer (I mean, space before dev->size) enough for the write?
      if (writable_bytes <= dev->size - dev->rw_offset)
      {
        if (copy_from_user(dev->data + dev->rw_offset , buff, writable_bytes))
        {
          retval = -EFAULT;
          goto out;
        }
        retval = writable_bytes;
        if (writable_bytes < dev->size - dev->rw_offset)
          dev->rw_offset += writable_bytes;
        else
          dev->rw_offset = 0;
      }
      else //We exceed the buffer end so we need to start writing from the beginning
      {
        remaining_bytes = writable_bytes - (dev->size - dev->rw_offset);
        writable_bytes = dev->size - dev->rw_offset;
        if (copy_from_user(dev->data + dev->rw_offset, buff, writable_bytes))
        {
          retval = -EFAULT;
          goto out;
        }
        dev->rw_offset = 0;
        retval = writable_bytes;
        if (copy_from_user(dev->data + dev->rw_offset, buff + writable_bytes, remaining_bytes))
        {
          retval = -EFAULT;
          goto out;
        }
        retval += remaining_bytes;
        dev->rw_offset = remaining_bytes;
      }
    }
  out:
  {
    return retval;
  }
}

int dev_open(struct inode *inode, struct file *filp){
  //struct wh_dev *wh_dev_num; /* device information */
  //wh_dev_num = container_of(inode->i_cdev, struct wh_dev, my_cdev);
  filp->private_data = &wh_dev;
  //printk(KERN_INFO "Major from inode  = %i\n",inode->i_rdev);
  return 0;
}
int dev_release(struct inode *inode, struct file *filp){
  return 0;
}
int dev_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg){
  struct wh_dev *dev = filp->private_data;
  switch(cmd){
    case (GET_INFO):{
      int writable_bytes = 0;
      ///Buffer sizes check
      if(dev->rw_offset < dev->rd_offset) 
        writable_bytes = dev->rd_offset - dev->rw_offset - 1;
      else if(dev->rw_offset > dev->rd_offset) 
        writable_bytes = dev->size - (dev->rw_offset - dev->rd_offset);
        //*(unsigned int*) arg = writable_bytes;
      printk("writable_bytes = %i\trw_offset = %i\trd_offset = %i\tsize = %i\n", 
              writable_bytes, dev->rw_offset, (int) dev->rd_offset, (int)dev->size);
      break;
    }
    case (RESET):{
      dev->rd_offset=0;
      dev->rw_offset=0;
      memset(dev->data, 0, wh_dev.size);
      printk("Buffer erased\n");
      break;
    }
    default:{
      break;
    }
  }
  return 0;
}
